#!/usr/bin/env node
/* eslint-env node */

const { rollup } = require('rollup');
const rollupBabel = require('@rollup/plugin-babel').default;
const rollupNodeResolve = require('@rollup/plugin-node-resolve').default;

const assert = require('assert');
const fs = require('fs');
const util = require('util');

const sourceFilename = 'src/webview/js/js.js';
const outputFilename = 'src/webview/js/generatedEs3.js';

/**
 * The minimum browser versions we support for the WebView.
 *
 * These should agree with the block comment at the top of js.js.
 * See also docs/architecture/platform-versions.md.
 */
/*
  Fun tip for seeing which transforms will stop being needed on an upgrade:
    $ jq -c '
         to_entries | map(.value + { name: .key })
         | sort_by(.chrome // "999" | tonumber)  # or (.ios // "999" | tonumber)
         | .[] | { chrome, safari, ios, name }
       ' node_modules/@babel/compat-data/data/plugins.json
*/
const targetPlatforms = {
  // Babel doesn't distinguish Chrome for Android vs. other Chrome.
  // See their data table, in node_modules/@babel/compat-data/data/plugins.json .
  //
  // Keep this at the Chrome version originally shipped with the oldest
  // Android version we support.
  chrome: 51,

  // (Babel accepts an "android" key here too.  But that refers to the old
  // Android System WebView, which Android 5 replaced with an auto-updating
  // Chrome; see our b3eced058.  So there's no such thing as "android: 5" or
  // later, and because we don't support older Android versions than that,
  // we don't use the "android" key.)

  // Babel does distinguish `safari` (meaning on macOS, I guess) from `ios`.
  // Some features arrive in e.g. "safari 11.1" but "ios 11.3", or in
  // 13.1 vs 13.4, so the distinction is still relevant.
  ios: 14,
};

// Disable the default react-native transformations.
// See `$ROOT/babel.config.js`.
process.env.BABEL_ENV = 'webview';

// node's fs predates async/await, alas
const writeFileAsync = util.promisify(fs.writeFile);

/**
 * Transform the transpiled code into the text of a module which exports that
 * code as a string.
 */
const wrapCode = es3Code => {
  /* eslint-disable-next-line no-useless-concat */
  const generatedMarker = '@' + 'generated';

  return `/*
 * This is a GENERATED file -- do not edit.
 * To make changes:
 *   1. Edit \`js.js\` or the files it imports, which together make up
 *      the source for this file.
 *   2. Run \`tools/generate-webview-js\`.
 *
 * ${generatedMarker}
 * @flow strict
 */

export default \`
'use strict';

${es3Code.replace(/\\/g, '\\\\')}
\`;
`;
};

(async () => {
  // Define the build process.
  const build = await rollup({
    input: sourceFilename,
    plugins: [
      rollupNodeResolve(),
      rollupBabel({
        exclude: 'node_modules/**',
        /* Our global config file applies React-Native-supplied and -targeted
           transforms. Those are somewhat counterproductive here. */
        configFile: false,
        babelrc: false,

        // 'bundled' is the default, but it's recommended to set it
        // explicitly; see
        // https://github.com/rollup/plugins/tree/master/packages/babel#babelhelpers
        babelHelpers: 'bundled',

        compact: false,
        comments: false,
        plugins: [
          ['@babel/transform-flow-strip-types'],

          /* Add TC39 stage 3+ transforms here as needed. */
          ['@babel/plugin-proposal-optional-chaining'],
        ],

        // See comments below about (the absence of) automatic polyfills via
        // `core-js`.
        presets: [
          [
            '@babel/preset-env',
            {
              targets: targetPlatforms,

              // Remove unescaped template-literal syntax in the output
              // string (backticks, string interpolation with ${}, etc.),
              // since for now we wrap the output in a template literal.
              // TODO(?): Remove that wrapping, and just send the output to
              //   a file which is read as a string. Or:
              // TODO(?): Properly escape template-literal syntax in the
              //   output string.
              include: ['@babel/plugin-transform-template-literals'],
            },
          ],
        ],
      }),
    ],
  });

  // Execute the build process.
  const { output } = await build.generate({
    format: 'iife' /* "immediately-invoked function expression" */,
    name: 'compiledWebviewJs',
    // TODO: eliminate `wrappedOutput`. See "TODO", below.
    /* file: outputFilename, */
  });

  // In general, Babel may return multiple output chunks. This particular
  // compilation setup, however, should return only a single chunk.
  assert.strictEqual(output.length, 1);

  // Extract and wrap that chunk's code.
  const wrappedOutput = wrapCode(output[0].code);

  // Write the transformed code.
  await writeFileAsync(outputFilename, wrappedOutput);
})().catch(
  // eslint-disable-next-line no-console
  console.error,
);

/* TODO:

  At present, `webview/script.js` wraps this script's output; in turn this
  script applies a wrapper around (the compiled text of) `webview/js.js`.

  This is at least one layer of abstraction more than we need.

  In the future, we'll probably want to make `js.js` compile to a single static
  asset file. Only a <script> tag and a one-to-three-line invocation should need
  to be emitted into the dynamically-rendered HTML.
*/

/**
 * # Babel, core-js and polyfills
 *
 * The `@babel/preset-env` package relies on the definitions and compatibility
 * declarations in `core-js` to select polyfills for rollup to include.
 *
 * Alas, this induces problems.
 *
 * The `core-js` package is dedicated to 100% compatibility, even on very old
 * (ES3) engines. This means that its compatibility profile includes polyfills
 * for marginal (read: irrelevant) functionality like `@@species` support
 * [c#644].
 *
 * Furthermore, the compatibility table checks aren't run recursively on the
 * dependencies of those polyfills; instead, the polyfills' dependencies are
 * polyfilled automatically. [t] `core-js`'s feature-detection _should_ prevent
 * these unnecessary polyfills from actually attaching to global objects; but
 * Rollup is entirely oblivious to this, and will (as directed by `core-js`)
 * cheerfully push into the final bundle all sorts of unnecessary-but-
 * `require()`d polyfills containing all sorts of support functionality that,
 * due to their runtime checks, can't be tree-shaken away. [c#388]
 *
 * It is possible to exclude individual polyfills on an item-by-item basis
 * [b.excl]; but this will only block detection at the top level by
 * @babel/preset-env. Even `core-js`'s own exclusion system [c.cloa] will not
 * honor internal polyfill block requests. [c#388]
 *
 * It's bad enough that `core-js` inflates the bundle with things like IE8- bug
 * workarounds [t] [c#388] and unstripped comments [t], and that it replaces
 * fast, sufficient native code with complex, less-JITtable code. Worse, though,
 * is that its aggressive replacement of native functionality can introduce
 * slowdown even on formally-unrelated code paths. [b#5771] [c#306]
 *
 * For all that, `core-js` _does_ work. Using it, or not using it, is a
 * tradeoff... but then, so is supporting older WebViews at all.
 *
 * See docs/architecture/platform-versions.md for details about our
 * version-support strategy.
 */

/**
 * Footnotes (separated to keep the paragraph rewrapper from trying to wrap
 * them):
 *
 *   [b#5771] https://github.com/babel/babel/issues/5771
 *   [b.excl] https://babeljs.io/docs/en/babel-preset-env#exclude
 *   [c#306] https://github.com/zloirock/core-js/issues/306
 *   [c#388] https://github.com/zloirock/core-js/issues/388
 *   [c#644] https://github.com/zloirock/core-js/issues/644
 *   [c.cloa] https://github.com/zloirock/core-js#configurable-level-of-aggressiveness
 *   [t] Observed in tests.
 */
